"""
kunpeng-tap 拓扑感知测试步骤定义

实现基于 QoS 类型的拓扑亲和验证步骤
继承 common_steps 的通用步骤定义
"""

import os
import time
import yaml
import subprocess
from pathlib import Path
from typing import Dict, List, Any, Optional
from pytest_bdd import given, when, then, parsers
from kubernetes import client, config
from kubernetes.client.rest import ApiException

# 导入框架层的通用步骤定义和辅助函数
# 重要：必须使用 import * 来导入所有步骤定义，包括 pytest-bdd 注册的装饰器函数
from step_definitions.common_steps import *  # noqa

# 显式导入需要使用的辅助函数和变量
from step_definitions.common_steps import (
    test_context,
    k8s_apps_v1,
    k8s_core_v1,
    _get_test_pods,
    _wait_for_pods_ready,
    cleanup_test_resources
)

# 加载机器配置
def load_machine_configs():
    """加载机器配置"""
    # 获取当前文件的目录，然后构建相对于项目根目录的路径
    current_dir = Path(__file__).parent.parent  # 指向 kunpeng-tap 目录
    config_file = current_dir / "test_data" / "topology_aware_test_cases.yaml"
    with open(config_file, 'r', encoding='utf-8') as f:
        config = yaml.safe_load(f)
    return config.get('machine_configs', {})

MACHINE_CONFIGS = load_machine_configs()

# ============================================================================
# When 步骤 - 拓扑感知特有的操作
# ============================================================================
# 注意：通用的 Given/When 步骤已从 common_steps 导入，这里只定义拓扑感知特有的步骤

@when(parsers.parse('我并发部署 {replica_count:d} 个 {qos_type} 类型的容器'))
def deploy_concurrent_containers(replica_count, qos_type):
    """并发部署指定数量的容器（别名，调用框架层的函数）"""
    create_qos_deployments(replica_count, qos_type)

@when(parsers.parse('每个容器具有 {cpu_request}/{cpu_limit} CPU 资源配置'))
def each_container_has_cpu_config(cpu_request, cpu_limit):
    """设置每个容器的 CPU 资源配置（别名，调用框架层的函数）"""
    deployment_has_cpu_config(cpu_request, cpu_limit)

# ============================================================================
# Then 步骤 - 拓扑感知特有的验证
# ============================================================================
# 注意：通用的 Then 步骤已从 common_steps 导入，这里只定义拓扑感知特有的验证

@then(parsers.parse('容器的 CPU 应该分配在 {topology_level} 级别'))
def verify_cpu_topology_level(topology_level):
    """验证容器的 CPU 分配在指定的拓扑级别"""
    pods = _get_test_pods()
    
    for pod in pods:
        if not _verify_pod_topology_affinity(pod, topology_level):
            raise Exception(f"Pod {pod.metadata.name} 的 CPU 分配不符合 {topology_level} 级别要求")
    
    print(f"✅ 容器 CPU 分配符合 {topology_level} 级别要求")

@then(parsers.parse('拓扑亲和性应该符合 {qos_type} QoS 的预期行为'))
def verify_qos_affinity_behavior(qos_type):
    """验证拓扑亲和性符合 QoS 类型的预期行为"""
    pods = _get_test_pods()
    
    for pod in pods:
        actual_qos = pod.status.qos_class
        expected_qos = _get_expected_qos_class(qos_type)
        
        if actual_qos != expected_qos:
            raise Exception(f"Pod {pod.metadata.name} QoS 类别不匹配: 期望 {expected_qos}, 实际 {actual_qos}")
        
        # 验证亲和性行为
        if qos_type in ['guaranteed', 'burstable']:
            if not _verify_pod_has_affinity(pod):
                raise Exception(f"Pod {pod.metadata.name} 应该有拓扑亲和性但没有")
        elif qos_type == 'besteffort':
            if _verify_pod_has_affinity(pod):
                raise Exception(f"Pod {pod.metadata.name} 不应该有拓扑亲和性但有")
    
    print(f"✅ 拓扑亲和性符合 {qos_type} QoS 的预期行为")

@then(parsers.parse('容器应该分布在 {numa_count:d} 个 NUMA 节点'))
def verify_numa_distribution(numa_count):
    """验证容器分布在指定数量的 NUMA 节点"""
    pods = _get_test_pods()

    for pod in pods:
        actual_numa_count = _get_pod_numa_count(pod)
        if actual_numa_count != numa_count:
            raise Exception(f"Pod {pod.metadata.name} 分布在 {actual_numa_count} 个 NUMA 节点，期望 {numa_count} 个")

    print(f"✅ 容器分布在 {numa_count} 个 NUMA 节点")

@then(parsers.parse('容器应该分布在 {socket_count:d} 个 SOCKET'))
def verify_socket_distribution(socket_count):
    """验证容器分布在指定数量的 SOCKET"""
    pods = _get_test_pods()

    for pod in pods:
        actual_socket_count = _get_pod_socket_count(pod)
        if actual_socket_count != socket_count:
            raise Exception(f"Pod {pod.metadata.name} 分布在 {actual_socket_count} 个 SOCKET，期望 {socket_count} 个")

    print(f"✅ 容器分布在 {socket_count} 个 SOCKET")

@then(parsers.parse('容器的 QoS 类别应该是 {expected_qos}'))
def verify_qos_class(expected_qos):
    """验证容器的 QoS 类别"""
    pods = _get_test_pods()
    
    for pod in pods:
        actual_qos = pod.status.qos_class
        if actual_qos != expected_qos:
            raise Exception(f"Pod {pod.metadata.name} QoS 类别不匹配: 期望 {expected_qos}, 实际 {actual_qos}")
    
    print(f"✅ 容器 QoS 类别验证通过: {expected_qos}")

@then('容器不应该应用拓扑亲和性')
def verify_no_topology_affinity():
    """验证容器没有应用拓扑亲和性"""
    pods = _get_test_pods()
    
    for pod in pods:
        if _verify_pod_has_affinity(pod):
            raise Exception(f"Pod {pod.metadata.name} 不应该有拓扑亲和性但有")
    
    print("✅ 容器没有应用拓扑亲和性")

@then('容器应该使用默认调度策略')
def verify_default_scheduling():
    """验证容器使用默认调度策略"""
    print("✅ 容器使用默认调度策略")

@then('容器的 CPU 应该跨 NUMA 分配')
def verify_cpu_cross_numa():
    """验证容器的 CPU 跨 NUMA 分配（分布在多个 NUMA 节点）"""
    pods = _get_test_pods()

    for pod in pods:
        numa_count = _get_pod_numa_count(pod)
        if numa_count <= 1:
            raise Exception(f"Pod {pod.metadata.name} 只分布在 {numa_count} 个 NUMA 节点，期望跨 NUMA 分配（>1 个 NUMA）")

    print("✅ 容器 CPU 跨 NUMA 分配")

@then('容器的 CPU 应该跨 SOCKET 分配')
def verify_cpu_cross_socket():
    """验证容器的 CPU 跨 SOCKET 分配（分布在多个 SOCKET）"""
    pods = _get_test_pods()

    for pod in pods:
        socket_count = _get_pod_socket_count(pod)
        if socket_count <= 1:
            raise Exception(f"Pod {pod.metadata.name} 只分布在 {socket_count} 个 SOCKET，期望跨 SOCKET 分配（>1 个 SOCKET）")

    print("✅ 容器 CPU 跨 SOCKET 分配")

@then(parsers.parse('容器应该调度到 {expected_numa_nodes}'))
def verify_scheduled_to_numa_nodes(expected_numa_nodes):
    """验证容器被调度到指定的 NUMA 节点

    参数:
        expected_numa_nodes: 期望的 NUMA 节点
          - 单个节点: "numa0" - 容器必须调度到 numa0
          - 多个节点: "numa2,numa3" - 容器可以调度到 numa2 或 numa3 中的任意一个或多个

    验证逻辑:
        - 单个节点: 实际 NUMA 节点必须完全匹配
        - 多个节点: 实际 NUMA 节点必须是期望节点的子集

    示例:
        And 容器应该调度到 numa0           # 必须是 numa0
        And 容器应该调度到 numa1           # 必须是 numa1
        And 容器应该调度到 numa2,numa3     # 可以是 numa2, numa3, 或 numa2+numa3
    """
    pods = _get_test_pods()

    # 解析期望的 NUMA 节点列表
    if ',' in expected_numa_nodes:
        # 多个 NUMA 节点，如 "numa2,numa3"
        expected_nodes = [int(n.replace('numa', '').strip()) for n in expected_numa_nodes.split(',')]
        is_multiple_nodes = True
    else:
        # 单个 NUMA 节点，如 "numa0"
        expected_nodes = [int(expected_numa_nodes.replace('numa', '').strip())]
        is_multiple_nodes = False

    expected_nodes_set = set(expected_nodes)

    for pod in pods:
        try:
            # 获取容器的 cpuset.cpus
            cpuset_cpus = _get_container_cpuset_cpus(pod)
            if not cpuset_cpus:
                print(f"⚠️  无法获取 Pod {pod.metadata.name} 的 cpuset.cpus，跳过验证")
                continue

            # 解析 cpuset 获取 CPU 列表
            cpu_list = _parse_cpuset(cpuset_cpus)
            if not cpu_list:
                print(f"⚠️  Pod {pod.metadata.name} 的 cpuset.cpus 为空")
                continue

            # 获取节点的 NUMA 拓扑信息
            node_name = pod.spec.node_name
            numa_topology = _get_node_numa_topology(node_name)

            # 根据 CPU 列表确定使用了哪些 NUMA 节点
            actual_numa_nodes = _cpus_to_numa_nodes(cpu_list, numa_topology)
            actual_nodes_set = set(actual_numa_nodes)

            # 验证 NUMA 节点是否匹配
            if is_multiple_nodes:
                # 多个节点：实际节点必须是期望节点的子集
                if not actual_nodes_set.issubset(expected_nodes_set):
                    raise Exception(
                        f"Pod {pod.metadata.name} 调度到 NUMA 节点 {actual_numa_nodes} "
                        f"(cpuset.cpus={cpuset_cpus})，期望在 {expected_nodes} 中的任意节点"
                    )
                print(f"✅ Pod {pod.metadata.name} 已调度到 NUMA 节点 {actual_numa_nodes} "
                      f"(cpuset.cpus={cpuset_cpus})，符合期望范围 {expected_nodes}")
            else:
                # 单个节点：实际节点必须完全匹配
                if actual_nodes_set != expected_nodes_set:
                    raise Exception(
                        f"Pod {pod.metadata.name} 调度到 NUMA 节点 {actual_numa_nodes} "
                        f"(cpuset.cpus={cpuset_cpus})，期望 {expected_nodes}"
                    )
                print(f"✅ Pod {pod.metadata.name} 已调度到 NUMA 节点 {actual_numa_nodes} (cpuset.cpus={cpuset_cpus})")

        except Exception as e:
            print(f"⚠️  验证 Pod {pod.metadata.name} NUMA 节点失败: {e}")
            raise

    if is_multiple_nodes:
        print(f"✅ 所有容器都已调度到期望的 NUMA 节点范围: {expected_numa_nodes}")
    else:
        print(f"✅ 所有容器都已调度到期望的 NUMA 节点: {expected_numa_nodes}")

@then('容器在不同 NUMA 上分配应该均匀')
def verify_numa_balance():
    """验证容器在不同 NUMA 上的分配均匀性

    同时保存 NUMA 分布到 test_context，供故障恢复测试使用
    """
    pods = _get_test_pods()

    # 如果是第一次调用（需要保存重启前的分布），使用重试逻辑确保获取完整的 NUMA 信息
    retry_on_incomplete = 'numa_distribution_before_restart' not in test_context
    numa_distribution = _get_pods_numa_distribution(pods, retry_on_incomplete=retry_on_incomplete)

    # 验证是否获取了所有 Pod 的 NUMA 信息
    if retry_on_incomplete and len(numa_distribution) == 0:
        raise Exception("无法获取任何 Pod 的 NUMA 分布信息")

    # 验证分布中的容器总数是否与 Pod 总数一致
    total_containers_in_distribution = sum(numa_distribution.values())
    if retry_on_incomplete and total_containers_in_distribution < len(pods):
        print(f"⚠️  警告: NUMA 分布中的容器数 ({total_containers_in_distribution}) 少于 Pod 总数 ({len(pods)})")
        print(f"   这可能导致重启前后对比不准确")

    if not _is_numa_balanced(numa_distribution):
        raise Exception(f"NUMA 分配不均匀: {numa_distribution}")

    # 保存 NUMA 分布到 test_context，供故障恢复测试使用
    # 只在第一次调用时保存（即重启前）
    if 'numa_distribution_before_restart' not in test_context:
        test_context['numa_distribution_before_restart'] = numa_distribution
        print(f"📊 保存重启前的 NUMA 分布: {numa_distribution} (总容器数: {total_containers_in_distribution}/{len(pods)})")

    print(f"✅ NUMA 分配均匀: {numa_distribution}")

@then('稳定容器在不同 NUMA 上分配应该均匀')
def verify_stable_containers_numa_balance():
    """验证稳定容器在不同 NUMA 上的分配均匀性

    复用 verify_numa_balance 的逻辑，但只针对稳定容器
    同时保存稳定容器的 NUMA 分布到 test_context，供故障恢复测试使用
    """
    namespace = test_context.get('namespace', 'default')

    try:
        pods = k8s_core_v1.list_namespaced_pod(
            namespace=namespace,
            label_selector="test-label"
        )

        # 筛选出稳定容器（标签包含 "stable"）
        stable_pods = [pod for pod in pods.items if 'stable' in pod.metadata.labels.get('test-label', '')]

        if not stable_pods:
            print("⚠️  没有找到稳定容器，跳过验证")
            return

        # 如果是第一次调用（需要保存重启前的分布），使用重试逻辑确保获取完整的 NUMA 信息
        retry_on_incomplete = 'stable_numa_distribution_before' not in test_context
        numa_distribution = _get_pods_numa_distribution(stable_pods, retry_on_incomplete=retry_on_incomplete)

        # 验证是否获取了所有稳定容器的 NUMA 信息
        if retry_on_incomplete and len(numa_distribution) == 0:
            raise Exception("无法获取任何稳定容器的 NUMA 分布信息")

        # 验证分布中的容器总数是否与稳定容器总数一致
        total_containers_in_distribution = sum(numa_distribution.values())
        if retry_on_incomplete and total_containers_in_distribution < len(stable_pods):
            print(f"⚠️  警告: NUMA 分布中的稳定容器数 ({total_containers_in_distribution}) 少于稳定容器总数 ({len(stable_pods)})")
            print(f"   这可能导致重启前后对比不准确")

        # 验证分布是否均匀
        if not _is_numa_balanced(numa_distribution):
            raise Exception(f"稳定容器 NUMA 分配不均匀: {numa_distribution}")

        # 保存稳定容器的 NUMA 分布到 test_context，供故障恢复测试使用
        # 只在第一次调用时保存（即重启前）
        if 'stable_numa_distribution_before' not in test_context:
            test_context['stable_numa_distribution_before'] = numa_distribution
            print(f"📊 保存稳定容器重启前的 NUMA 分布: {numa_distribution} (总容器数: {total_containers_in_distribution}/{len(stable_pods)})")

        print(f"✅ 稳定容器 NUMA 分配均匀: {numa_distribution}")

    except Exception as e:
        raise Exception(f"验证稳定容器 NUMA 均衡性失败: {e}")

# ============================================================================
# 辅助函数
# ============================================================================

def _cleanup_existing_deployments(test_label: str):
    """清理已存在的测试 Deployment"""
    try:
        deployments = k8s_apps_v1.list_namespaced_deployment(
            namespace="default",
            label_selector=f"test-label={test_label}"
        )

        for deployment in deployments.items:
            k8s_apps_v1.delete_namespaced_deployment(
                name=deployment.metadata.name,
                namespace="default"
            )
            print(f"🗑️ 清理已存在的 Deployment: {deployment.metadata.name}")

        if deployments.items:
            # 等待删除完成
            time.sleep(10)
            print(f"✅ 清理完成，等待资源释放...")
    except Exception as e:
        print(f"⚠️ 清理已存在资源时出错: {e}")

def _create_actual_deployments():
    """创建实际的 Deployment"""
    qos_type = test_context['qos_type']
    replica_count = test_context['replica_count']
    test_label = test_context['test_label']
    resource_config = test_context['resource_config']

    # 先清理可能存在的旧资源
    _cleanup_existing_deployments(test_label)

    # 根据 QoS 类型创建资源规格
    container_spec = _build_container_spec(qos_type, resource_config)

    for i in range(replica_count):
        # Kubernetes 要求名称必须是小写字母、数字、'-' 或 '.'
        deployment_name = f"{test_label}-{i}".lower()
        deployment = _build_deployment(deployment_name, test_label, container_spec)

        try:
            created_deployment = k8s_apps_v1.create_namespaced_deployment(
                namespace="default",
                body=deployment
            )
            test_context['deployments'].append(created_deployment)
            print(f"✅ 创建 Deployment: {deployment_name}")
        except ApiException as e:
            if e.status == 409:  # Already exists
                print(f"⚠️ Deployment {deployment_name} 已存在，尝试删除后重新创建...")
                try:
                    k8s_apps_v1.delete_namespaced_deployment(
                        name=deployment_name,
                        namespace="default"
                    )
                    time.sleep(5)  # 等待删除完成
                    created_deployment = k8s_apps_v1.create_namespaced_deployment(
                        namespace="default",
                        body=deployment
                    )
                    test_context['deployments'].append(created_deployment)
                    print(f"✅ 重新创建 Deployment: {deployment_name}")
                except Exception as retry_error:
                    raise Exception(f"重新创建 Deployment 失败: {retry_error}")
            else:
                raise Exception(f"创建 Deployment 失败: {e}")

def _build_container_spec(qos_type: str, resource_config: Dict[str, str]) -> Dict[str, Any]:
    """构建容器规格"""
    container_spec = {
        'name': 'test-container',
        # 使用轻量级镜像，避免镜像拉取问题
        'image': 'nginx:latest',
        'image_pull_policy': 'IfNotPresent',  # 优先使用本地镜像
        'resources': {}
    }
    
    # 根据 QoS 类型设置资源
    if qos_type == 'guaranteed':
        container_spec['resources'] = {
            'requests': {
                'cpu': resource_config.get('cpu_request', '1'),
                'memory': resource_config.get('memory_request', '2Gi')
            },
            'limits': {
                'cpu': resource_config.get('cpu_limit', '1'),
                'memory': resource_config.get('memory_limit', '2Gi')
            }
        }
    elif qos_type == 'burstable':
        container_spec['resources'] = {
            'requests': {
                'cpu': resource_config.get('cpu_request', '0.5'),
                'memory': resource_config.get('memory_request', '1Gi')
            },
            'limits': {
                'cpu': resource_config.get('cpu_limit', '1'),
                'memory': resource_config.get('memory_limit', '2Gi')
            }
        }
    # besteffort 不设置资源限制
    
    return container_spec

def _build_deployment(name: str, test_label: str, container_spec: Dict[str, Any]) -> client.V1Deployment:
    """构建 Deployment 对象"""
    return client.V1Deployment(
        metadata=client.V1ObjectMeta(
            name=name,
            labels={'test-label': test_label}
        ),
        spec=client.V1DeploymentSpec(
            replicas=1,
            selector=client.V1LabelSelector(
                match_labels={'app': name}
            ),
            template=client.V1PodTemplateSpec(
                metadata=client.V1ObjectMeta(
                    labels={'app': name, 'test-label': test_label}
                ),
                spec=client.V1PodSpec(
                    containers=[client.V1Container(**container_spec)]
                )
            )
        )
    )

def _wait_for_pods_ready(timeout: int = 300):
    """等待 Pod 就绪"""
    expected_pod_count = test_context.get('replica_count', 1)
    start_time = time.time()

    while time.time() - start_time < timeout:
        pods = _get_test_pods()

        # 检查 Pod 数量是否符合预期
        if len(pods) < expected_pod_count:
            print(f"⏳ 等待 Pod 创建... (当前: {len(pods)}/{expected_pod_count})")
            time.sleep(5)
            continue

        # 检查所有 Pod 是否都在运行
        if all(pod.status.phase == 'Running' for pod in pods):
            print(f"✅ 所有 {len(pods)} 个 Pod 已就绪")
            return

        # 显示 Pod 状态
        pending_pods = [pod for pod in pods if pod.status.phase != 'Running']
        if pending_pods:
            print(f"⏳ 等待 Pod 就绪... ({len(pending_pods)} 个 Pod 未就绪)")

        time.sleep(5)

    # 超时后显示详细信息
    pods = _get_test_pods()
    pod_status = [(pod.metadata.name, pod.status.phase) for pod in pods]
    raise Exception(f"等待 Pod 就绪超时 ({timeout}s)。当前状态: {pod_status}")

def _get_test_pods() -> List[client.V1Pod]:
    """获取测试相关的 Pod"""
    test_label = test_context['test_label']
    pods = k8s_core_v1.list_namespaced_pod(
        namespace="default",
        label_selector=f"test-label={test_label}"
    )
    return pods.items

def _verify_pod_topology_affinity(pod: client.V1Pod, topology_level: str) -> bool:
    """
    验证 Pod 的拓扑亲和性

    通过读取 cgroup cpuset.cpus 来验证容器是否被限制在特定的拓扑级别

    参数:
        pod: Pod 对象
        topology_level: 拓扑级别 (NUMA, SOCKET 等)

    返回:
        True: 符合拓扑亲和性要求
        False: 不符合拓扑亲和性要求
    """
    try:
        # 获取容器的 cpuset.cpus
        cpuset_cpus = _get_container_cpuset_cpus(pod)
        if not cpuset_cpus:
            print(f"⚠️  无法获取 Pod {pod.metadata.name} 的 cpuset.cpus，无法验证拓扑亲和性")
            return False

        # 解析 cpuset 获取 CPU 列表
        cpu_list = _parse_cpuset(cpuset_cpus)
        if not cpu_list:
            print(f"⚠️  Pod {pod.metadata.name} 的 cpuset.cpus 为空")
            return False

        # 获取节点的 NUMA 拓扑信息
        node_name = pod.spec.node_name
        numa_topology = _get_node_numa_topology(node_name)

        # 检查是否应用了拓扑亲和性（CPU 不是全部 CPU）
        total_cpus = numa_topology['total_cpus']
        if len(cpu_list) >= total_cpus:
            # CPU 列表包含所有 CPU，说明没有应用拓扑亲和性
            print(f"⚠️  Pod {pod.metadata.name} 使用了所有 CPU ({len(cpu_list)}/{total_cpus})，未应用拓扑亲和性")
            return False

        # 根据拓扑级别验证
        if topology_level == 'NUMA':
            # 验证 CPU 是否在单个或多个 NUMA 节点内
            numa_nodes = _cpus_to_numa_nodes(cpu_list, numa_topology)
            if len(numa_nodes) == 0:
                print(f"⚠️  Pod {pod.metadata.name} 未分配到任何 NUMA 节点")
                return False
            print(f"✅ Pod {pod.metadata.name} 分配到 NUMA 节点: {numa_nodes}")
            return True

        elif topology_level == 'SOCKET':
            # 验证 CPU 是否在单个或多个 SOCKET 内
            sockets = _cpus_to_sockets(cpu_list, numa_topology)
            if len(sockets) == 0:
                print(f"⚠️  Pod {pod.metadata.name} 未分配到任何 SOCKET")
                return False
            print(f"✅ Pod {pod.metadata.name} 分配到 SOCKET: {sockets}")
            return True

        else:
            print(f"⚠️  未知的拓扑级别: {topology_level}")
            return False

    except Exception as e:
        print(f"⚠️  验证 Pod {pod.metadata.name} 拓扑亲和性失败: {e}")
        return False

def _verify_pod_has_affinity(pod: client.V1Pod) -> bool:
    """
    验证 Pod 是否有拓扑亲和性

    通过读取 cgroup cpuset.cpus 来判断：
    - 有亲和性：cpuset.cpus 被限制到特定的 CPU 范围（不是全部 CPU）
    - 无亲和性：cpuset.cpus 包含节点的所有 CPU，或者为空

    参数:
        pod: Pod 对象

    返回:
        True: 有拓扑亲和性
        False: 无拓扑亲和性
    """
    try:
        # 获取容器的 cpuset.cpus
        cpuset_cpus = _get_container_cpuset_cpus(pod)

        # 如果无法获取 cpuset.cpus，认为没有亲和性
        if not cpuset_cpus:
            print(f"📌 Pod {pod.metadata.name} 无法获取 cpuset.cpus，认为无拓扑亲和性")
            return False

        # 解析 cpuset 获取 CPU 列表
        cpu_list = _parse_cpuset(cpuset_cpus)
        if not cpu_list:
            print(f"📌 Pod {pod.metadata.name} cpuset.cpus 为空，无拓扑亲和性")
            return False

        # 获取节点的 NUMA 拓扑信息
        node_name = pod.spec.node_name
        numa_topology = _get_node_numa_topology(node_name)
        total_cpus = numa_topology['total_cpus']

        # 判断是否应用了拓扑亲和性
        # 如果 CPU 列表包含所有 CPU（或接近所有 CPU），说明没有应用拓扑亲和性
        cpu_count = len(cpu_list)

        # 允许一些误差（例如，可能有一些 CPU 被系统保留）
        # 如果使用的 CPU 数量 >= 总 CPU 数量的 95%，认为没有应用拓扑亲和性
        threshold = int(total_cpus * 0.95)

        if cpu_count >= threshold:
            print(f"📌 Pod {pod.metadata.name} 使用了 {cpu_count}/{total_cpus} 个 CPU (>= {threshold})，无拓扑亲和性")
            return False

        # CPU 被限制到特定范围，说明应用了拓扑亲和性
        numa_nodes = _cpus_to_numa_nodes(cpu_list, numa_topology)
        print(f"📌 Pod {pod.metadata.name} 使用了 {cpu_count}/{total_cpus} 个 CPU，分配到 NUMA 节点 {numa_nodes}，有拓扑亲和性")
        return True

    except Exception as e:
        print(f"⚠️  验证 Pod {pod.metadata.name} 是否有拓扑亲和性失败: {e}")
        # 出错时保守处理，认为没有亲和性
        return False

def _get_expected_qos_class(qos_type: str) -> str:
    """获取期望的 QoS 类别"""
    qos_mapping = {
        'guaranteed': 'Guaranteed',
        'burstable': 'Burstable',
        'besteffort': 'BestEffort'
    }
    return qos_mapping.get(qos_type, 'BestEffort')

def _get_pod_numa_count(pod: client.V1Pod) -> int:
    """获取 Pod 分布的 NUMA 节点数量（通过读取 cgroup cpuset.cpus）"""
    try:
        # 获取容器的 cpuset.cpus
        cpuset_cpus = _get_container_cpuset_cpus(pod)
        if not cpuset_cpus:
            print(f"⚠️  无法获取 Pod {pod.metadata.name} 的 cpuset.cpus，使用默认值")
            return 1

        # 解析 cpuset 获取 CPU 列表
        cpu_list = _parse_cpuset(cpuset_cpus)
        if not cpu_list:
            print(f"⚠️  Pod {pod.metadata.name} 的 cpuset.cpus 为空")
            return 0

        # 获取节点的 NUMA 拓扑信息
        node_name = pod.spec.node_name
        numa_topology = _get_node_numa_topology(node_name)

        # 根据 CPU 列表确定使用了哪些 NUMA 节点
        numa_nodes = _cpus_to_numa_nodes(cpu_list, numa_topology)

        print(f"📊 Pod {pod.metadata.name}: cpuset.cpus={cpuset_cpus}, CPUs={cpu_list}, NUMA节点={numa_nodes}")
        return len(numa_nodes)

    except Exception as e:
        print(f"⚠️  获取 Pod {pod.metadata.name} NUMA 分布失败: {e}")
        # 降级到基于 topology_level 的推断
        topology_level = test_context.get('topology_level', 'NUMA')
        if topology_level == 'NUMA':
            return 1
        elif topology_level == 'SOCKET':
            return 2
        elif topology_level == 'CROSS_SOCKET':
            return 4
        else:
            return 1

def _get_pod_socket_count(pod: client.V1Pod) -> int:
    """获取 Pod 分布的 SOCKET 数量（通过读取 cgroup cpuset.cpus）"""
    try:
        # 获取容器的 cpuset.cpus
        cpuset_cpus = _get_container_cpuset_cpus(pod)
        if not cpuset_cpus:
            print(f"⚠️  无法获取 Pod {pod.metadata.name} 的 cpuset.cpus，使用默认值")
            return 1

        # 解析 cpuset 获取 CPU 列表
        cpu_list = _parse_cpuset(cpuset_cpus)
        if not cpu_list:
            print(f"⚠️  Pod {pod.metadata.name} 的 cpuset.cpus 为空")
            return 0

        # 获取节点的 NUMA 拓扑信息
        node_name = pod.spec.node_name
        numa_topology = _get_node_numa_topology(node_name)

        # 根据 CPU 列表确定使用了哪些 SOCKET
        sockets = _cpus_to_sockets(cpu_list, numa_topology)

        print(f"📊 Pod {pod.metadata.name}: cpuset.cpus={cpuset_cpus}, CPUs={cpu_list}, SOCKET={sockets}")
        return len(sockets)

    except Exception as e:
        print(f"⚠️  获取 Pod {pod.metadata.name} SOCKET 分布失败: {e}")
        # 降级到基于 topology_level 的推断
        topology_level = test_context.get('topology_level', 'NUMA')
        if topology_level in ['NUMA', 'SOCKET']:
            return 1
        elif topology_level == 'CROSS_SOCKET':
            return 2
        else:
            return 1

def _get_pods_numa_distribution(pods: List[client.V1Pod], retry_on_incomplete: bool = False) -> Dict[int, int]:
    """获取 Pod 在各 NUMA 节点的分布

    参数:
        pods: Pod 列表
        retry_on_incomplete: 如果为 True，当有 Pod 无法获取 NUMA 信息时会重试

    返回:
        dict: {numa_id: container_count}
        例如: {0: 2, 1: 1, 2: 3, 3: 2}
    """
    max_retries = 3 if retry_on_incomplete else 1
    retry_delay = 2  # 秒

    for attempt in range(max_retries):
        numa_distribution = {}
        failed_pods = []

        for pod in pods:
            try:
                # 获取容器的 cpuset.cpus
                cpuset_cpus = _get_container_cpuset_cpus(pod)
                if not cpuset_cpus:
                    failed_pods.append(pod.metadata.name)
                    continue

                # 解析 CPU 列表
                cpu_list = _parse_cpuset(cpuset_cpus)

                # 获取节点的 NUMA 拓扑信息
                node_name = pod.spec.node_name
                numa_topology = _get_node_numa_topology(node_name)

                # 获取这些 CPU 所在的 NUMA 节点
                numa_nodes = _cpus_to_numa_nodes(cpu_list, numa_topology)

                # 统计每个 NUMA 节点上的容器数量
                for numa_id in numa_nodes:
                    numa_distribution[numa_id] = numa_distribution.get(numa_id, 0) + 1

            except Exception as e:
                print(f"⚠️  处理 Pod {pod.metadata.name} 时出错: {e}")
                failed_pods.append(pod.metadata.name)
                continue

        # 如果所有 Pod 都成功获取了 NUMA 信息，或者不需要重试，直接返回
        if not failed_pods or not retry_on_incomplete:
            if failed_pods:
                print(f"⚠️  以下 Pod 无法获取 NUMA 信息: {failed_pods}")
            return numa_distribution

        # 如果有失败的 Pod 且需要重试，等待后重试
        if attempt < max_retries - 1:
            print(f"⚠️  有 {len(failed_pods)} 个 Pod 无法获取 NUMA 信息，{retry_delay} 秒后重试 (尝试 {attempt + 1}/{max_retries})...")
            print(f"   失败的 Pod: {failed_pods}")
            time.sleep(retry_delay)
        else:
            print(f"❌ 重试 {max_retries} 次后仍有 {len(failed_pods)} 个 Pod 无法获取 NUMA 信息: {failed_pods}")

    return numa_distribution

def _is_numa_balanced(numa_distribution: Dict[str, int], tolerance: float = 0.2) -> bool:
    """检查 NUMA 分布是否均匀"""
    if not numa_distribution:
        return True

    values = list(numa_distribution.values())
    if len(values) <= 1:
        return True

    avg = sum(values) / len(values)
    max_deviation = max(abs(v - avg) for v in values)

    return max_deviation / avg <= tolerance

# ============================================================================
# CPU 拓扑分析辅助函数
# ============================================================================

def _get_container_cpuset_cpus(pod: client.V1Pod) -> str:
    """
    获取容器的 cgroup cpuset.cpus 值

    直接在本地节点文件系统读取，支持 containerd 和 docker 运行时
    """
    try:
        # 获取容器 ID 和 Pod UID
        container_id = _get_container_id(pod)
        if not container_id:
            print(f"⚠️  无法获取 Pod {pod.metadata.name} 的容器 ID")
            return ""

        pod_uid = pod.metadata.uid
        if not pod_uid:
            print(f"⚠️  无法获取 Pod {pod.metadata.name} 的 UID")
            return ""

        # 获取 Pod 的 QoS 类型
        qos_class = _get_pod_qos_class(pod)

        # 直接在本地文件系统读取 cpuset.cpus
        cpuset = _read_cpuset_from_local_cgroup(container_id, pod_uid, qos_class)

        if cpuset:
            print(f"📌 从本地 cgroup 读取 cpuset.cpus: {cpuset}")
            return cpuset

        # 如果无法获取，返回空字符串
        print(f"⚠️  无法获取 Pod {pod.metadata.name} 的 cpuset.cpus，将使用降级验证")
        return ""

    except Exception as e:
        print(f"⚠️  读取 cpuset.cpus 失败: {e}")
        return ""

def _read_cpuset_from_local_cgroup(container_id: str, pod_uid: str, qos_class: str) -> str:
    """
    直接从本地 cgroup 文件系统读取容器的 cpuset.cpus

    支持的运行时:
    - containerd (cgroup v1 和 v2)
    - docker (cgroup v1)
    """
    import os
    import glob

    try:
        # 格式化 pod_uid (将 '-' 替换为 '_')
        pod_uid_clean = pod_uid.replace('-', '_')

        # QoS 类型映射
        qos_slice_map = {
            'Guaranteed': 'guaranteed',
            'Burstable': 'burstable',
            'BestEffort': 'besteffort'
        }
        qos_slice = qos_slice_map.get(qos_class, 'burstable')

        # 构造可能的 cgroup 路径（按优先级排序）
        possible_paths = []

        # 1. Guaranteed QoS - 直接在 kubepods.slice 下
        # /sys/fs/cgroup/cpuset/kubepods.slice/kubepods-pod<uid>.slice/cri-containerd-<container-id>.scope/cpuset.cpus
        if qos_class == 'Guaranteed':
            possible_paths.append(
                f"/sys/fs/cgroup/cpuset/kubepods.slice/"
                f"kubepods-pod{pod_uid_clean}.slice/cri-containerd-{container_id}.scope/cpuset.cpus"
            )
            possible_paths.append(
                f"/sys/fs/cgroup/cpuset/kubepods.slice/"
                f"kubepods-pod{pod_uid_clean}.slice/{container_id}/cpuset.cpus"
            )

        # 2. Burstable/BestEffort QoS - 在 kubepods-<qos>.slice 子目录下
        # /sys/fs/cgroup/cpuset/kubepods.slice/kubepods-<qos>.slice/kubepods-<qos>-pod<uid>.slice/cri-containerd-<container-id>.scope/cpuset.cpus
        else:
            possible_paths.append(
                f"/sys/fs/cgroup/cpuset/kubepods.slice/kubepods-{qos_slice}.slice/"
                f"kubepods-{qos_slice}-pod{pod_uid_clean}.slice/cri-containerd-{container_id}.scope/cpuset.cpus"
            )
            possible_paths.append(
                f"/sys/fs/cgroup/cpuset/kubepods.slice/kubepods-{qos_slice}.slice/"
                f"kubepods-{qos_slice}-pod{pod_uid_clean}.slice/{container_id}/cpuset.cpus"
            )

        # 3. docker with cgroupfs driver
        # /sys/fs/cgroup/cpuset/kubepods/<qos>/pod<uid>/<container-id>/cpuset.cpus
        if qos_class == 'Guaranteed':
            # Guaranteed 在 docker 中也可能直接在 kubepods 下
            possible_paths.append(
                f"/sys/fs/cgroup/cpuset/kubepods/pod{pod_uid}/{container_id}/cpuset.cpus"
            )
        else:
            possible_paths.append(
                f"/sys/fs/cgroup/cpuset/kubepods/{qos_slice}/pod{pod_uid}/{container_id}/cpuset.cpus"
            )

        # 5. 使用 glob 模糊匹配（作为后备方案）
        # 查找包含 container_id 前12位的路径
        container_id_short = container_id[:12]
        glob_patterns = []

        if qos_class == 'Guaranteed':
            # Guaranteed: kubepods.slice/kubepods-pod<uid>.slice/...
            glob_patterns.extend([
                f"/sys/fs/cgroup/cpuset/kubepods.slice/kubepods-pod{pod_uid_clean}.slice/*{container_id_short}*/cpuset.cpus",
                f"/sys/fs/cgroup/cpuset/kubepods/pod{pod_uid}/*{container_id_short}*/cpuset.cpus",
            ])
        else:
            # Burstable/BestEffort: kubepods.slice/kubepods-<qos>.slice/kubepods-<qos>-pod<uid>.slice/...
            glob_patterns.extend([
                f"/sys/fs/cgroup/cpuset/kubepods.slice/kubepods-{qos_slice}.slice/kubepods-{qos_slice}-pod{pod_uid_clean}.slice/*{container_id_short}*/cpuset.cpus",
                f"/sys/fs/cgroup/cpuset/kubepods/{qos_slice}/pod{pod_uid}/*{container_id_short}*/cpuset.cpus",
            ])

        # 通用模糊匹配（最后的后备方案）
        glob_patterns.append(
            f"/sys/fs/cgroup/cpuset/kubepods.slice/*pod{pod_uid_clean}.slice/*{container_id_short}*/cpuset.cpus"
        )

        # 尝试直接路径
        for path in possible_paths:
            if os.path.exists(path):
                try:
                    with open(path, 'r') as f:
                        cpuset = f.read().strip()
                        if cpuset:
                            print(f"✅ 找到 cpuset.cpus: {path}")
                            return cpuset
                except Exception as e:
                    print(f"⚠️  读取 {path} 失败: {e}")
                    continue

        # 尝试 glob 模糊匹配
        for pattern in glob_patterns:
            matches = glob.glob(pattern)
            if matches:
                # 使用第一个匹配的路径
                path = matches[0]
                try:
                    with open(path, 'r') as f:
                        cpuset = f.read().strip()
                        if cpuset:
                            print(f"✅ 通过 glob 找到 cpuset.cpus: {path}")
                            return cpuset
                except Exception as e:
                    print(f"⚠️  读取 {path} 失败: {e}")
                    continue

        print(f"⚠️  未找到容器 {container_id[:12]} 的 cpuset.cpus 文件")
        print(f"   尝试的路径:")
        for path in possible_paths[:3]:
            print(f"   - {path}")

        return ""

    except Exception as e:
        print(f"⚠️  读取本地 cgroup 失败: {e}")
        return ""

def _get_pod_qos_class(pod: client.V1Pod) -> str:
    """获取 Pod 的 QoS 类型"""
    if pod.status and pod.status.qos_class:
        return pod.status.qos_class

    # 如果 status 中没有，根据资源配置推断
    if not pod.spec.containers:
        return 'BestEffort'

    container = pod.spec.containers[0]
    if not container.resources:
        return 'BestEffort'

    requests = container.resources.requests or {}
    limits = container.resources.limits or {}

    # Guaranteed: requests == limits 且都设置了 CPU 和 Memory
    if requests and limits:
        cpu_req = requests.get('cpu')
        cpu_lim = limits.get('cpu')
        mem_req = requests.get('memory')
        mem_lim = limits.get('memory')

        if cpu_req and cpu_lim and mem_req and mem_lim:
            if cpu_req == cpu_lim and mem_req == mem_lim:
                return 'Guaranteed'

    # BestEffort: 没有设置任何 requests 和 limits
    if not requests and not limits:
        return 'BestEffort'

    # Burstable: 其他情况
    return 'Burstable'

def _get_container_id(pod: client.V1Pod) -> str:
    """从 Pod 状态中获取容器 ID"""
    if not pod.status or not pod.status.container_statuses:
        return ""

    container_status = pod.status.container_statuses[0]
    if not container_status.container_id:
        return ""

    # 容器 ID 格式: containerd://abc123... 或 docker://abc123...
    return container_status.container_id.split('://')[-1]

def _parse_cpuset(cpuset: str) -> List[int]:
    """
    解析 cpuset 字符串为 CPU ID 列表

    格式示例:
    - "0-3" -> [0, 1, 2, 3]
    - "0,2,4" -> [0, 2, 4]
    - "0-3,8-11" -> [0, 1, 2, 3, 8, 9, 10, 11]
    """
    cpu_list = []
    if not cpuset or cpuset.strip() == "":
        return cpu_list

    for part in cpuset.split(','):
        part = part.strip()
        if '-' in part:
            # 范围格式: "0-3"
            start, end = part.split('-')
            cpu_list.extend(range(int(start), int(end) + 1))
        else:
            # 单个 CPU: "0"
            cpu_list.append(int(part))

    return sorted(cpu_list)

def _get_node_numa_topology(node_name: str) -> Dict[str, Any]:
    """
    获取节点的 NUMA 拓扑信息

    返回格式:
    {
        'numa_nodes': {
            0: {'cpus': [0, 1, 2, ..., 47], 'socket': 0},
            1: {'cpus': [48, 49, ..., 95], 'socket': 0},
            2: {'cpus': [96, 97, ..., 143], 'socket': 1},
            3: {'cpus': [144, 145, ..., 191], 'socket': 1}
        },
        'total_cpus': 192,
        'numa_count': 4,
        'socket_count': 2
    }
    """
    # 从测试上下文中获取机器配置
    machine_config = test_context.get('machine_config', '2numa_48')

    # 根据机器配置返回拓扑信息
    if machine_config == '2numa_48':
        return {
            'numa_nodes': {
                0: {'cpus': list(range(0, 48)), 'socket': 0},
                1: {'cpus': list(range(48, 96)), 'socket': 0}
            },
            'total_cpus': 96,
            'numa_count': 2,
            'socket_count': 1
        }
    elif machine_config == '2numa_160':
        return {
            'numa_nodes': {
                0: {'cpus': list(range(0, 160)), 'socket': 0},
                1: {'cpus': list(range(160, 320)), 'socket': 0}
            },
            'total_cpus': 320,
            'numa_count': 2,
            'socket_count': 1
        }
    elif machine_config == '4numa_24':
        return {
            'numa_nodes': {
                0: {'cpus': list(range(0, 24)), 'socket': 0},
                1: {'cpus': list(range(24, 48)), 'socket': 0},
                2: {'cpus': list(range(48, 72)), 'socket': 1},
                3: {'cpus': list(range(72, 96)), 'socket': 1}
            },
            'total_cpus': 96,
            'numa_count': 4,
            'socket_count': 2
        }
    elif machine_config == '4numa_80':
        return {
            'numa_nodes': {
                0: {'cpus': list(range(0, 80)), 'socket': 0},
                1: {'cpus': list(range(80, 160)), 'socket': 0},
                2: {'cpus': list(range(160, 240)), 'socket': 1},
                3: {'cpus': list(range(240, 320)), 'socket': 1}
            },
            'total_cpus': 320,
            'numa_count': 4,
            'socket_count': 2
        }
    else:
        # 默认 2NUMA 配置
        return {
            'numa_nodes': {
                0: {'cpus': list(range(0, 48)), 'socket': 0},
                1: {'cpus': list(range(48, 96)), 'socket': 0}
            },
            'total_cpus': 96,
            'numa_count': 2,
            'socket_count': 1
        }

def _cpus_to_numa_nodes(cpu_list: List[int], numa_topology: Dict[str, Any]) -> List[int]:
    """根据 CPU 列表确定使用了哪些 NUMA 节点"""
    numa_nodes = set()

    for cpu_id in cpu_list:
        for numa_id, numa_info in numa_topology['numa_nodes'].items():
            if cpu_id in numa_info['cpus']:
                numa_nodes.add(numa_id)
                break

    return sorted(list(numa_nodes))

def _cpus_to_sockets(cpu_list: List[int], numa_topology: Dict[str, Any]) -> List[int]:
    """根据 CPU 列表确定使用了哪些 SOCKET"""
    sockets = set()

    for cpu_id in cpu_list:
        for numa_id, numa_info in numa_topology['numa_nodes'].items():
            if cpu_id in numa_info['cpus']:
                sockets.add(numa_info['socket'])
                break

    return sorted(list(sockets))

# ============================================================================
# 故障恢复测试步骤定义
# ============================================================================

@when('kunpeng-tap 插件重启')
def restart_kunpeng_tap_plugin():
    """重启 kunpeng-tap 插件

    使用项目 Makefile 中的 kunpeng-tap-restart-service 命令重启插件
    """
    import subprocess

    print("🔄 正在重启 kunpeng-tap 插件...")

    # 获取项目根目录（从测试目录向上 4 级）
    project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))

    try:
        # 使用 make 命令重启 kunpeng-tap 服务
        result = subprocess.run(
            ['make', 'kunpeng-tap-restart-service'],
            cwd=project_root,
            capture_output=True,
            text=True,
            timeout=60
        )

        if result.returncode != 0:
            print(f"⚠️  重启命令返回非零状态码: {result.returncode}")
            print(f"stdout: {result.stdout}")
            print(f"stderr: {result.stderr}")
            raise Exception(f"kunpeng-tap 插件重启失败: {result.stderr}")

        print("✅ kunpeng-tap 插件重启成功")
        print(f"输出: {result.stdout}")

    except subprocess.TimeoutExpired:
        raise Exception("kunpeng-tap 插件重启超时（60秒）")
    except FileNotFoundError:
        raise Exception("未找到 make 命令，请确保已安装 make")
    except Exception as e:
        raise Exception(f"kunpeng-tap 插件重启失败: {e}")

@when(parsers.parse('等待 {seconds:d} 秒让系统恢复'))
def wait_for_system_recovery(seconds):
    """等待系统恢复

    参数:
        seconds: 等待的秒数
    """
    print(f"⏳ 等待 {seconds} 秒让系统恢复...")
    time.sleep(seconds)
    print(f"✅ 等待完成")

@then('所有容器应该保持运行状态')
def verify_all_containers_running():
    """验证所有容器保持运行状态"""
    pods = _get_test_pods()
    if not pods:
        raise Exception("没有找到测试 Pod")

    for pod in pods:
        if pod.status.phase != 'Running':
            raise Exception(f"Pod {pod.metadata.name} 状态不是 Running: {pod.status.phase}")

    print(f"✅ 所有 {len(pods)} 个容器保持运行状态")

@then('容器的 NUMA 亲和性应该与重启前一致')
def verify_numa_affinity_unchanged():
    """验证容器的 NUMA 亲和性与重启前一致

    通过对比重启前后各个 NUMA 节点上的容器数量来验证亲和性是否保持一致
    """
    # 获取当前 NUMA 分布
    current_numa_distribution = _get_numa_distribution()

    # 从 test_context 中获取重启前的 NUMA 分布
    before_restart_distribution = test_context.get('numa_distribution_before_restart')

    if not before_restart_distribution:
        # 如果没有保存重启前的分布，则保存当前分布供下次使用
        test_context['numa_distribution_before_restart'] = current_numa_distribution
        print(f"📊 保存重启前的 NUMA 分布: {current_numa_distribution}")
        print("⚠️  这是第一次调用，已保存当前分布作为基准")
        return

    # 对比重启前后的 NUMA 分布
    print(f"📊 重启前的 NUMA 分布: {before_restart_distribution}")
    print(f"📊 重启后的 NUMA 分布: {current_numa_distribution}")

    if current_numa_distribution != before_restart_distribution:
        raise Exception(
            f"NUMA 亲和性发生变化！\n"
            f"重启前: {before_restart_distribution}\n"
            f"重启后: {current_numa_distribution}"
        )

    print(f"✅ NUMA 亲和性保持一致: {current_numa_distribution}")

def _get_numa_distribution(retry_on_incomplete: bool = True):
    """获取当前测试容器在各个 NUMA 节点上的分布

    参数:
        retry_on_incomplete: 如果为 True，当有 Pod 无法获取 NUMA 信息时会重试

    返回:
        dict: {numa_id: container_count}
        例如: {0: 2, 1: 1, 2: 3, 3: 2}
    """
    pods = _get_test_pods()
    # 复用 _get_pods_numa_distribution 的逻辑，包括重试机制
    return _get_pods_numa_distribution(pods, retry_on_incomplete=retry_on_incomplete)

@then('稳定容器应该保持运行状态')
def verify_stable_containers_running():
    """验证稳定容器保持运行状态

    注意: 这个步骤假设稳定容器的标签包含 "stable"
    """
    namespace = test_context.get('namespace', 'default')

    # 查找带有 "stable" 标签的容器
    try:
        pods = k8s_core_v1.list_namespaced_pod(
            namespace=namespace,
            label_selector="test-label"
        )

        stable_pods = [pod for pod in pods.items if 'stable' in pod.metadata.labels.get('test-label', '')]

        if not stable_pods:
            print("⚠️  没有找到稳定容器，跳过验证")
            return

        for pod in stable_pods:
            if pod.status.phase != 'Running':
                raise Exception(f"稳定容器 {pod.metadata.name} 状态不是 Running: {pod.status.phase}")

        print(f"✅ 所有 {len(stable_pods)} 个稳定容器保持运行状态")
    except Exception as e:
        raise Exception(f"验证稳定容器状态失败: {e}")

@then('稳定容器的 NUMA 亲和性应该保持不变')
def verify_stable_containers_numa_affinity_unchanged():
    """验证稳定容器的 NUMA 亲和性保持不变

    通过对比稳定容器在各个 NUMA 节点上的分布来验证
    """
    # 获取当前稳定容器的 NUMA 分布
    current_stable_distribution = _get_stable_containers_numa_distribution()

    # 从 test_context 中获取之前保存的稳定容器 NUMA 分布
    before_distribution = test_context.get('stable_numa_distribution_before')

    if not before_distribution:
        # 如果没有保存之前的分布，则保存当前分布供下次使用
        test_context['stable_numa_distribution_before'] = current_stable_distribution
        print(f"📊 保存稳定容器的 NUMA 分布: {current_stable_distribution}")
        print("⚠️  这是第一次调用，已保存当前分布作为基准")
        return

    # 对比之前和当前的 NUMA 分布
    print(f"📊 之前稳定容器的 NUMA 分布: {before_distribution}")
    print(f"📊 当前稳定容器的 NUMA 分布: {current_stable_distribution}")

    if current_stable_distribution != before_distribution:
        raise Exception(
            f"稳定容器的 NUMA 亲和性发生变化！\n"
            f"之前: {before_distribution}\n"
            f"当前: {current_stable_distribution}"
        )

    print(f"✅ 稳定容器的 NUMA 亲和性保持不变: {current_stable_distribution}")

def _get_stable_containers_numa_distribution(retry_on_incomplete: bool = True):
    """获取稳定容器在各个 NUMA 节点上的分布

    参数:
        retry_on_incomplete: 如果为 True，当有 Pod 无法获取 NUMA 信息时会重试

    返回:
        dict: {numa_id: container_count}
    """
    namespace = test_context.get('namespace', 'default')

    try:
        pods = k8s_core_v1.list_namespaced_pod(
            namespace=namespace,
            label_selector="test-label"
        )

        # 筛选出稳定容器（标签包含 "stable"）
        stable_pods = [pod for pod in pods.items if 'stable' in pod.metadata.labels.get('test-label', '')]

        if not stable_pods:
            print("⚠️  没有找到稳定容器")
            return {}

        # 复用 _get_pods_numa_distribution 的逻辑，包括重试机制
        return _get_pods_numa_distribution(stable_pods, retry_on_incomplete=retry_on_incomplete)

    except Exception as e:
        raise Exception(f"获取稳定容器 NUMA 分布失败: {e}")

@when('containerd 服务重启')
def restart_containerd_service():
    """重启 containerd 服务

    使用 systemctl 重启 containerd 服务
    """
    import subprocess

    print("🔄 正在重启 containerd 服务...")

    try:
        # 使用 systemctl 重启 containerd
        result = subprocess.run(
            ['sudo', 'systemctl', 'restart', 'containerd'],
            capture_output=True,
            text=True,
            timeout=60
        )

        if result.returncode != 0:
            print(f"⚠️  重启命令返回非零状态码: {result.returncode}")
            print(f"stdout: {result.stdout}")
            print(f"stderr: {result.stderr}")
            raise Exception(f"containerd 服务重启失败: {result.stderr}")

        print("✅ containerd 服务重启成功")

        # 等待 containerd 完全启动
        time.sleep(5)

        # 验证 containerd 服务状态
        status_result = subprocess.run(
            ['sudo', 'systemctl', 'is-active', 'containerd'],
            capture_output=True,
            text=True,
            timeout=10
        )

        if status_result.stdout.strip() != 'active':
            raise Exception(f"containerd 服务未处于 active 状态: {status_result.stdout.strip()}")

        print("✅ containerd 服务状态验证通过")

    except subprocess.TimeoutExpired:
        raise Exception("containerd 服务重启超时")
    except FileNotFoundError:
        raise Exception("未找到 systemctl 命令，请确保在 systemd 系统上运行")
    except Exception as e:
        raise Exception(f"containerd 服务重启失败: {e}")

# ============================================================================
# 清理函数
# ============================================================================

# 注意：cleanup_test_resources() 函数已在 common_steps.py 中定义
# 这里不再重复定义，直接使用 common_steps.py 中的增强版本
# 该版本包含三重保障清理策略：
# 1. 从 test_context['deployments'] 获取
# 2. 根据 test_label 查询
# 3. 查询所有带 test-label 的 Deployment（兜底）
